/**
 * Copyright (C) 2021-2022 maminjie <canpool@163.com>
 * SPDX-License-Identifier: LGPL-2.1
 * Powered by Qt-Advanced-Docking-System
**/

#include "dockpannel.h"

#include "dockmanager_p.h"
#include "docktabbar.h"
#include "docktitlebar.h"
#include "dockcontainer.h"
#include "dockmanager.h"
#include "dockoverlay.h"
#include "docksplitter.h"
#include "dockwidget.h"
#include "docktab.h"
#include "dockfloatingwidget.h"

#include <QList>
#include <QLoggingCategory>
#include <QMenu>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QSplitter>
#include <QStackedLayout>
#include <QStyle>
#include <QVector>
#include <QWheelEvent>
#include <QXmlStreamWriter>

QADS_BEGIN_NAMESPACE

/**
 * Internal dock area layout mimics stack layout but only inserts the current
 * widget into the internal QLayout object.
 * \warning Only the current widget has a parent. All other widgets
 * do not have a parent. That means, a widget that is in this layout may
 * return nullptr for its parent() function if it is not the current widget.
 */
class DockAreaLayout
{
public:
    /**
     * Creates an instance with the given parent layout
     */
    DockAreaLayout(QBoxLayout *parentLayout)
        : m_parentLayout(parentLayout)
    {}

    /**
     * Returns the number of widgets in this layout
     */
    int count() const {
        return m_widgets.count();
    }

    /**
     * Inserts the widget at the given index position into the internal widget
     * list
     */
    void insertWidget(int index, QWidget *widget) {
        widget->setParent(nullptr);

        if (index < 0) {
            index = m_widgets.count();
        }

        m_widgets.insert(index, widget);

        if (m_currentIndex < 0) {
            setCurrentIndex(index);
        } else {
            if (index <= m_currentIndex) {
                ++m_currentIndex;
            }
        }
    }

    /**
     * Removes the given widget from the layout
     */
    void removeWidget(QWidget *widget) {
        if (currentWidget() == widget) {
            auto layoutItem = m_parentLayout->takeAt(1);
            if (layoutItem) {
                layoutItem->widget()->setParent(nullptr);
            }

            m_currentWidget = nullptr;
            m_currentIndex = -1;
        } else if (indexOf(widget) < m_currentIndex) {
            --m_currentIndex;
        }

        m_widgets.removeOne(widget);
    }

    /**
     * Returns the current selected widget
     */
    QWidget *currentWidget() const {
        return m_currentWidget;
    }

    /**
     * Activates the widget with the give index.
     */
    void setCurrentIndex(int index) {
        QWidget *prev = currentWidget();
        QWidget *next = widget(index);

        if (!next || (next == prev && !m_currentWidget)) {
            return;
        }

        bool reenableUpdates = false;
        QWidget *parent = m_parentLayout->parentWidget();

        if (parent && parent->updatesEnabled()) {
            reenableUpdates = true;
            parent->setUpdatesEnabled(false);
        }

        if (auto layoutItem = m_parentLayout->takeAt(1)) {
            layoutItem->widget()->setParent(nullptr);
        }
        m_parentLayout->addWidget(next);

        if (prev) {
            prev->hide();
        }

        m_currentIndex = index;
        m_currentWidget = next;

        if (reenableUpdates) {
            parent->setUpdatesEnabled(true);
        }
    }

    /**
     * Returns the index of the current active widget
     */
    int currentIndex() const {
        return m_currentIndex;
    }

    /**
     * Returns true if there are no widgets in the layout
     */
    bool isEmpty() const {
        return m_widgets.empty();
    }

    /**
     * Returns the index of the given widget
     */
    int indexOf(QWidget *widget) const {
        return m_widgets.indexOf(widget);
    }

    /**
     * Returns the widget for the given index
     */
    QWidget *widget(int index) const {
        return (index < m_widgets.size()) ? m_widgets.at(index) : nullptr;
    }

    /**
     * Returns the geometry of the current active widget
     */
    QRect geometry() const {
        return m_widgets.empty() ? QRect() : currentWidget()->geometry();
    }

private:
    QBoxLayout *m_parentLayout;
    QList<QWidget *> m_widgets;
    int m_currentIndex = -1;
    QWidget *m_currentWidget = nullptr;
};

using DockPannelLayout = QStackedLayout;
static const DockWidgetAreas DefaultAllowedAreas = AllDockAreas;

/* DockPannelPrivate */
class DockPannelPrivate
{
public:
    DockPannelPrivate(DockPannel *parent);

    void init();
    void createTitleBar();
    DockTabBar *tabBar() const;
    void updateTitleBarButtonStates();
    void updateMinimumSizeHint();

public:
    DockPannel *q = nullptr;
    QBoxLayout *m_layout = nullptr;
    DockPannelLayout *m_contentsLayout = nullptr;
    DockTitleBar *m_titleBar = nullptr;
    DockManager *m_dockManager = nullptr;
    bool m_updateTitleBarButtons = false;
    DockWidgetAreas m_allowedAreas = DefaultAllowedAreas;
    DockPannel::DockPannelFlags m_flags { DockPannel::DefaultFlags };
    QSize m_minSizeHint;
};

DockPannelPrivate::DockPannelPrivate(DockPannel *parent)
    : q(parent)
{}

void DockPannelPrivate::init()
{
    m_layout = new QBoxLayout(QBoxLayout::TopToBottom);
    m_layout->setContentsMargins(0, 0, 0, 0);
    m_layout->setSpacing(0);
    q->setLayout(m_layout);
    createTitleBar();
    m_contentsLayout = new DockPannelLayout(m_layout);

    if (m_dockManager) {
        emit m_dockManager->dockPannelCreated(q);
    }
}

void DockPannelPrivate::createTitleBar()
{
    m_titleBar = new DockTitleBar(q);
    m_layout->addWidget(m_titleBar);
    QObject::connect(tabBar(), &DockTabBar::tabCloseRequested, q, &DockPannel::onTabCloseRequested);
    QObject::connect(m_titleBar, &DockTitleBar::tabBarClicked, q, &DockPannel::setCurrentIndex);
    QObject::connect(tabBar(), &DockTabBar::tabMoved, q, &DockPannel::reorderDockWidget);
}

DockTabBar *DockPannelPrivate::tabBar() const
{
    return m_titleBar->tabBar();
}

/**
 * Updates the enable state of the close and detach button
 */
void DockPannelPrivate::updateTitleBarButtonStates()
{
    if (q->isHidden()) {
        m_updateTitleBarButtons = true;
        return;
    }

    m_titleBar->button(CloseButton)->setEnabled(q->features().testFlag(DockWidget::DockWidgetClosable));
    m_titleBar->button(UndockButton)->setEnabled(q->features().testFlag(DockWidget::DockWidgetFloatable));
    m_titleBar->updateDockWidgetActionsButtons();
    m_updateTitleBarButtons = false;
}

/**
 * Scans all contained dock widgets for the max. minimum size hint
 */
void DockPannelPrivate::updateMinimumSizeHint()
{
    m_minSizeHint = QSize();

    for (int i = 0; i < m_contentsLayout->count(); ++i) {
        auto widget = m_contentsLayout->widget(i);
        m_minSizeHint.setHeight(qMax(m_minSizeHint.height(), widget->minimumSizeHint().height()));
        m_minSizeHint.setWidth(qMax(m_minSizeHint.width(), widget->minimumSizeHint().width()));
    }
}

DockPannel::DockPannel(DockManager *dockManager, DockContainer *parent)
    : QWidget(parent)
    , d(new DockPannelPrivate(this))
{
    Q_ASSERT(dockManager);
    setAttribute(Qt::WA_StyledBackground, true);
    d->m_dockManager = dockManager;
    d->init();
}

DockPannel::~DockPannel()
{
    delete d->m_contentsLayout;
    delete d;
}

DockManager *DockPannel::dockManager() const
{
    return d->m_dockManager;
}

DockContainer *DockPannel::dockContainer() const
{
    return internal::findParent<DockContainer *>(this);
}

DockTitleBar *DockPannel::titleBar() const
{
    return d->m_titleBar;
}

QRect DockPannel::titleBarGeometry() const
{
    return d->m_titleBar->geometry();
}

QAbstractButton *DockPannel::titleBarButton(TitleBarButtonRole which) const
{
    return d->m_titleBar->button(which);
}

int DockPannel::dockWidgetsCount() const
{
    return d->m_contentsLayout->count();
}

QList<DockWidget *> DockPannel::dockWidgets() const
{
    QList<DockWidget *> dockWidgetList;

    for (int i = 0; i < d->m_contentsLayout->count(); ++i) {
        dockWidgetList.append(dockWidget(i));
    }

    return dockWidgetList;
}

DockWidget *DockPannel::dockWidget(int index) const
{
    return qobject_cast<DockWidget *>(d->m_contentsLayout->widget(index));
}

int DockPannel::openDockWidgetsCount() const
{
    int count = 0;

    for (int i = 0; i < d->m_contentsLayout->count(); ++i) {
        if (!dockWidget(i)->isClosed()) {
            ++count;
        }
    }

    return count;
}

QList<DockWidget *> DockPannel::openedDockWidgets() const
{
    QList<DockWidget *> dockWidgetList;

    for (int i = 0; i < d->m_contentsLayout->count(); ++i) {
        DockWidget *currentDockWidget = dockWidget(i);
        if (!currentDockWidget->isClosed()) {
            dockWidgetList.append(dockWidget(i));
        }
    }

    return dockWidgetList;
}

int DockPannel::indexOfFirstOpenDockWidget() const
{
    for (int i = 0; i < d->m_contentsLayout->count(); ++i) {
        if (!dockWidget(i)->isClosed()) {
            return i;
        }
    }
    return -1;
}

DockWidget *DockPannel::currentDockWidget() const
{
    int currentIdx = currentIndex();
    if (currentIdx < 0) {
        return nullptr;
    }

    return dockWidget(currentIdx);
}

void DockPannel::setCurrentDockWidget(DockWidget *dockWidget)
{
    if (dockManager()->isRestoringState()) {
        return;
    }

    setCurrentDockWidgetInternal(dockWidget);
}

DockWidgetAreas DockPannel::allowedAreas() const
{
    return d->m_allowedAreas;
}

void DockPannel::setAllowedAreas(DockWidgetAreas areas)
{
    d->m_allowedAreas = areas;
}

DockWidget::DockWidgetFeatures DockPannel::features(BitwiseOperator mode) const
{
    QList<DockWidget *> dws = dockWidgets();

    if (BitwiseAnd == mode) {
        DockWidget::DockWidgetFeatures features(DockWidget::AllDockWidgetFeatures);
        for (const auto dw : qAsConst(dws)) {
            features &= dw->features();
        }
        return features;
    } else {
        DockWidget::DockWidgetFeatures features(DockWidget::NoDockWidgetFeatures);
        for (const auto dw : qAsConst(dws)) {
            features |= dw->features();
        }
        return features;
    }
}

DockPannel::DockPannelFlags DockPannel::dockPannelFlags() const
{
    return d->m_flags;
}

void DockPannel::setDockPannelFlags(DockPannelFlags flags)
{
    auto changedFlags = d->m_flags ^ flags;
    d->m_flags = flags;
    if (changedFlags.testFlag(HideSingleWidgetTitleBar)){
        updateTitleBarVisibility();
    }
}

void DockPannel::setDockPannelFlag(DockPannel::DockPannelFlag flag, bool on)
{
    auto flags = dockPannelFlags();
    internal::setFlag(flags, flag, on);
    setDockPannelFlags(flags);
}

/**
 * Returns true if the pannel contains the central widget of it's manager.
 */
bool DockPannel::isCentralPannel() const
{
    if (dockWidgetsCount() != 1)  {
        return false;
    }

    return dockManager()->centralWidget() == dockWidgets()[0];
}

int DockPannel::currentIndex() const
{
    return d->m_contentsLayout->currentIndex();
}

void DockPannel::setCurrentIndex(int index)
{
    auto currentTabBar = d->tabBar();

    if (index < 0 || index > (currentTabBar->count() - 1)) {
        return;
    }

    auto cw = d->m_contentsLayout->currentWidget();
    auto nw = d->m_contentsLayout->widget(index);

    if (cw == nw && !nw->isHidden()) {
        return;
    }

    emit currentChanging(index);
    currentTabBar->setCurrentIndex(index);
    d->m_contentsLayout->setCurrentIndex(index);
    d->m_contentsLayout->currentWidget()->show();
    emit currentChanged(index);
}

void DockPannel::closePannel()
{
    // If there is only one single dock widget and this widget has the
    // DeleteOnClose feature or CustomCloseHandling, then we delete the dock widget now;
    // in the case of CustomCloseHandling, the CDockWidget class will emit its
    // closeRequested signal and not actually delete unless the signal is handled in a way that deletes it
    auto openDockWidgets = openedDockWidgets();

    if (openDockWidgets.count() == 1 &&
        (openDockWidgets[0]->features().testFlag(DockWidget::DockWidgetDeleteOnClose)  ||
         openDockWidgets[0]->features().testFlag(DockWidget::CustomCloseHandling))) {
        openDockWidgets[0]->closeDockWidgetInternal();
    } else {
        for (auto dockWidget : qAsConst(openDockWidgets)) {
            if ((dockWidget->features().testFlag(DockWidget::DockWidgetDeleteOnClose) &&
                dockWidget->features().testFlag(DockWidget::DockWidgetForceCloseWithArea)) ||
                dockWidget->features().testFlag(DockWidget::CustomCloseHandling)) {
                dockWidget->closeDockWidgetInternal();
            } else {
                dockWidget->toggleView(false);
            }
        }
    }
}

void DockPannel::closeOtherPannels()
{
    dockContainer()->closeOtherPannels(this);
}

QSize DockPannel::minimumSizeHint() const
{
    if (!d->m_minSizeHint.isValid()) {
        return QWidget::minimumSizeHint();
    }
    if (d->m_titleBar->isVisible()) {
        return d->m_minSizeHint + QSize(0, d->m_titleBar->minimumSizeHint().height());
    } else {
        return d->m_minSizeHint;
    }
}

QRect DockPannel::contentAreaGeometry() const
{
    return d->m_contentsLayout->geometry();
}

void DockPannel::setVisible(bool visible)
{
    QWidget::setVisible(visible);

    if (d->m_updateTitleBarButtons) {
        d->updateTitleBarButtonStates();
    }
}

void DockPannel::saveState(QXmlStreamWriter &stream) const
{
    stream.writeStartElement("pannel");
    stream.writeAttribute("tabs", QString::number(d->m_contentsLayout->count()));
    auto localDockWidget = currentDockWidget();
    QString name = localDockWidget ? localDockWidget->objectName() : "";
    stream.writeAttribute("current", name);
    // To keep the saved XML data small, we only save the allowed areas and the
    // dock area flags if the values are different from the default values
    if (d->m_allowedAreas != DefaultAllowedAreas) {
        stream.writeAttribute("allowedAreas", QString::number(d->m_allowedAreas, 16));
    }
    if (d->m_flags != DefaultFlags) {
        stream.writeAttribute("flags", QString::number(d->m_flags, 16));
    }
    for (int i = 0; i < d->m_contentsLayout->count(); ++i) {
        dockWidget(i)->saveState(stream);
    }

    stream.writeEndElement();
}

void DockPannel::insertDockWidget(int index, DockWidget *dockWidget, bool activate)
{
    d->m_contentsLayout->insertWidget(index, dockWidget);
    dockWidget->setDockPannel(this);
    DockTab *tab = dockWidget->tab();
    // Inserting the tab will change the current index which in turn will
    // make the tab widget visible in the slot
    d->tabBar()->blockSignals(true);
    d->tabBar()->insertTab(index, tab);
    d->tabBar()->blockSignals(false);
    tab->setVisible(!dockWidget->isClosed());
    d->m_minSizeHint.setHeight(qMax(d->m_minSizeHint.height(), dockWidget->minimumSizeHint().height()));
    d->m_minSizeHint.setWidth(qMax(d->m_minSizeHint.width(), dockWidget->minimumSizeHint().width()));

    if (activate) {
        setCurrentIndex(index);
    }

    // If this dock area is hidden, then we need to make it visible again
    // by calling dockWidget->toggleViewInternal(true);
    if (!this->isVisible() && d->m_contentsLayout->count() > 1 && !dockManager()->isRestoringState()) {
        dockWidget->toggleViewInternal(true);
    }

    d->updateTitleBarButtonStates();
    updateTitleBarVisibility();
}

void DockPannel::addDockWidget(DockWidget *dockWidget)
{
    insertDockWidget(d->m_contentsLayout->count(), dockWidget);
}

void DockPannel::removeDockWidget(DockWidget *dockWidget)
{
    auto currentDockWidget = this->currentDockWidget();
    auto nextOpen = (dockWidget == currentDockWidget) ? nextOpenDockWidget(dockWidget) : nullptr;
    d->m_contentsLayout->removeWidget(dockWidget);
    auto tab = dockWidget->tab();
    tab->hide();
    d->tabBar()->removeTab(tab);
    tab->setParent(dockWidget);
    dockWidget->setDockPannel(nullptr);

    DockContainer *dockContainerWidget = dockContainer();

    if (nextOpen) {
        setCurrentDockWidget(nextOpen);
    } else if (d->m_contentsLayout->isEmpty() && dockContainerWidget->dockPannelCount() >= 1) {
        dockContainerWidget->removeDockPannel(this);
        this->deleteLater();
        if (dockContainerWidget->dockPannelCount() == 0) {
            if (DockFloatingWidget *floatinigWidget = dockContainerWidget->floatingWidget()) {
                floatinigWidget->hide();
                floatinigWidget->deleteLater();
            }
        }
    } else if (dockWidget == currentDockWidget) {
        // if contents layout is not empty but there are no more open dock
        // widgets, then we need to hide the dock area because it does not
        // contain any visible content
        hideWithNoVisibleContent();
    }

    d->updateTitleBarButtonStates();
    updateTitleBarVisibility();
    d->updateMinimumSizeHint();
    auto topLevelDockWidget = dockContainerWidget->topLevelDockWidget();

    if (topLevelDockWidget) {
        topLevelDockWidget->emitTopLevelChanged(true);
    }

#if (QADS_DEBUG_LEVEL > 0)
    dockContainerWidget->dumpLayout();
#endif
}

void DockPannel::toggleDockWidgetView(DockWidget *dockWidget, bool open)
{
    Q_UNUSED(dockWidget)
    Q_UNUSED(open)
    updateTitleBarVisibility();
}

void DockPannel::toggleView(bool open)
{
    setVisible(open);
    emit viewToggled(open);
}

DockWidget *DockPannel::nextOpenDockWidget(DockWidget *dockWidget) const
{
    auto openDockWidgets = openedDockWidgets();

    if (openDockWidgets.count() > 1 || (openDockWidgets.count() == 1 && openDockWidgets[0] != dockWidget)) {
        DockWidget *nextDockWidget;
        if (openDockWidgets.last() == dockWidget) {
            nextDockWidget = openDockWidgets[openDockWidgets.count() - 2];
            // search backwards for widget with tab
            for (int i = openDockWidgets.count() - 2; i >= 0; --i) {
                auto dw = openDockWidgets[i];
                if (!dw->features().testFlag(DockWidget::NoTab)) {
                    return dw;
                }
            }
        } else {
            int curIndex = openDockWidgets.indexOf(dockWidget);
            nextDockWidget = openDockWidgets[curIndex + 1];
            // search forwards for widget with tab
            for (int i = curIndex + 1; i < openDockWidgets.count(); ++i)  {
                auto dw = openDockWidgets[i];
                if (!dw->features().testFlag(DockWidget::NoTab)) {
                    return dw;
                }
            }

            // search backwards for widget with tab
            for (int i = curIndex - 1; i >= 0; --i) {
                auto dw = openDockWidgets[i];
                if (!dw->features().testFlag(DockWidget::NoTab)) {
                    return dw;
                }
            }
        }
        return nextDockWidget;
    } else {
        return nullptr;
    }
}

int DockPannel::indexOf(DockWidget *dockWidget)
{
    return d->m_contentsLayout->indexOf(dockWidget);
}

void DockPannel::hideWithNoVisibleContent()
{
    this->toggleView(false);
    // Hide empty parent splitters
    auto splitter = internal::findParent<DockSplitter *>(this);
    internal::hideEmptyParentSplitters(splitter);
    // Hide empty floating widget
    DockContainer *container = this->dockContainer();
    if (!container->isFloating() && QAds::testConfigFlag(QAds::HideSingleCentralWidgetTitleBar)) {
        return;
    }

    updateTitleBarVisibility();
    auto topLevelWidget = container->topLevelDockWidget();
    auto floatingWidget = container->floatingWidget();

    if (topLevelWidget) {
        if (floatingWidget) {
            floatingWidget->updateWindowTitle();
        }
        DockWidget::emitTopLevelEventForWidget(topLevelWidget, true);
    } else if (container->openedDockPannels().isEmpty() && floatingWidget) {
        floatingWidget->hide();
    }
}

void DockPannel::updateTitleBarVisibility()
{
    DockContainer *container = dockContainer();
    if (!container) {
        return;
    }

    if (QAds::testConfigFlag(QAds::AlwaysShowTabs)) {
        return;
    }

    if (d->m_titleBar) {
        bool hidden = container->hasTopLevelDockWidget()
                && (container->isFloating() || QAds::testConfigFlag(QAds::HideSingleCentralWidgetTitleBar));
        hidden |= (d->m_flags.testFlag(HideSingleWidgetTitleBar) && openDockWidgetsCount() == 1);
        d->m_titleBar->setVisible(!hidden);
    }
}

void DockPannel::setCurrentDockWidgetInternal(DockWidget *dockWidget)
{
    int index = indexOf(dockWidget);
    if (index < 0) {
        return;
    }

    setCurrentIndex(index);
}

void DockPannel::markTitleBarMenuOutdated()
{
    if (d->m_titleBar) {
        d->m_titleBar->markTabsMenuOutdated();
    }
}

#ifdef Q_OS_WIN
bool DockPannel::event(QEvent *e)
{
    switch (e->type()) {
    case QEvent::PlatformSurface:
        return true;
    default:
        break;
    }
    return QWidget::event(e);
}
#endif

void DockPannel::onTabCloseRequested(int index)
{
    auto *widget = dockWidget(index);

    if (widget->features().testFlag(DockWidget::DockWidgetDeleteOnClose) ||
        widget->features().testFlag(DockWidget::CustomCloseHandling)) {
        widget->closeDockWidgetInternal();
    } else {
        widget->toggleView(false);
    }
}

void DockPannel::reorderDockWidget(int fromIndex, int toIndex)
{
    if (fromIndex >= d->m_contentsLayout->count() || fromIndex < 0 ||
            toIndex >= d->m_contentsLayout->count() || toIndex < 0 || fromIndex == toIndex) {
        return;
    }

    auto widget = d->m_contentsLayout->widget(fromIndex);
    d->m_contentsLayout->removeWidget(widget);
    d->m_contentsLayout->insertWidget(toIndex, widget);
    setCurrentIndex(toIndex);
}

void DockPannel::onDockWidgetFeaturesChanged()
{
    if (d->m_titleBar) {
        d->updateTitleBarButtonStates();
    }
}

QADS_END_NAMESPACE
